#!/usr/bin/env python3

# Copyright (c) 2005-2014 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


import os
import re
import abc
import enum
import copy
import subprocess
import collections
from ._util import Util
from ._util import PhysicalDiskMounts
from ._exception import RunningEnvironmentError


class Flags(enum.Flag):
    BIOS = enum.auto()
    BOOTLOADER = enum.auto()
    KERNEL = enum.auto()
    INITRAMFS = enum.auto()


class KernelType(enum.Enum):
    LINUX = "linux"


class Arch(enum.Enum):
    X86_64 = "x86_64"


class BootMode(enum.Enum):
    EFI = "efi"
    BIOS = "bios"


class SystemInit:

    class Type(enum.Enum):
        SYSVINIT = "sysv-init"
        OPENRC = "openrc"
        SYSTEMD = "systemd"
        CUSTOM = "custom"

    def __init__(self, system_init_type, cmd=None):
        self.system_init_type = system_init_type
        if self.system_init_type == self.Type.SYSVINIT:
            # FIXME
            assert False
        elif self.system_init_type == self.Type.OPENRC:
            assert cmd is None
            self.cmd = "/sbin/openrc-init"
        elif self.system_init_type == self.Type.SYSTEMD:
            assert cmd is None
            self.cmd = "/usr/lib/systemd/systemd"
        elif self.system_init_type == self.Type.CUSTOM:
            assert cmd is not None
            self.cmd = cmd
        else:
            assert False

    def __eq__(self, other):
        if type(self) is not type(other):
            return False

        if self.system_init_type != other.system_init_type:
            return False

        if self.system_init_type == self.Type.SYSVINIT:
            assert self.cmd == other.cmd
        elif self.system_init_type == self.Type.OPENRC:
            assert self.cmd == other.cmd
        elif self.system_init_type == self.Type.SYSTEMD:
            assert self.cmd == other.cmd
        elif self.system_init_type == self.Type.CUSTOM:
            if self.cmd != other.cmd:
                return False
        else:
            assert False

        return True

    def __hash__(self):
        return hash((self.system_init_type, self.cmd))


class RescueOsSpec:

    def __init__(self, bbki):
        self.root_dir = bbki._fsLayout.get_boot_rescue_os_dir()
        self.kernel_filepath = os.path.join(self.root_dir, "vmlinuz")
        self.kernel_cmdline_filepath = os.path.join(self.root_dir, "vmlinuz.cmdline")
        self.initrd_filepath = os.path.join(self.root_dir, "initrd.img")

    def __repr__(self):
        return "<%s %r>" % (self.__class__.__name__, self.__dict__)


class HostMountPoint(collections.abc.Mapping):

    FS_TYPE_VFAT = "vfat"
    FS_TYPE_EXT4 = "ext4"
    FS_TYPE_BTRFS = "btrfs"
    FS_TYPE_BCACHEFS = "bcachefs"
    FS_TYPE_SQUASHFS_WITH_OVERLAY = "squashfs+overlay"

    def __init__(self, **kwargs):
        self._data = {}

        # self.mountpoint
        assert "mountpoint" in kwargs
        assert os.path.isabs(kwargs["mountpoint"])
        self._data["mountpoint"] = kwargs["mountpoint"]

        # self.device and self.dev_uuid
        if "dev_path_or_uuid" in kwargs:
            assert "device" not in kwargs and "dev_uuid" not in kwargs
            if re.fullmatch(r'UUID=\S+', kwargs["dev_path_or_uuid"]):
                self._data["device"] = None
                self._data["dev_uuid"] = kwargs["dev_path_or_uuid"]
            elif re.fullmatch(r'UUID:FILE=\S+', kwargs["dev_path_or_uuid"]):
                self._data["device"] = None
                self._data["dev_uuid"] = kwargs["dev_path_or_uuid"]
            elif ":" in kwargs["dev_path_or_uuid"]:
                tlist = kwargs["dev_path_or_uuid"].split(":")
                if all([item.startswith("/dev/") for item in tlist]):
                    self._data["device"] = kwargs["dev_path_or_uuid"]
                    self._data["dev_uuid"] = ":".join(["UUID_SUB=%s" % (Util.getBlkDevSubUuid(item)) for item in tlist])
                elif all([re.fullmatch(r'UUID_SUB=\S+', item) for item in tlist]):
                    self._data["device"] = None
                    self._data["dev_uuid"] = kwargs["dev_path_or_uuid"]
                else:
                    assert False
            elif kwargs["dev_path_or_uuid"].startswith("/dev/"):
                self._data["device"] = kwargs["dev_path_or_uuid"]
                self._data["dev_uuid"] = "UUID=%s" % (Util.getBlkDevUuid(kwargs["dev_path_or_uuid"]))
            else:
                assert False
        else:
            self._data["device"] = kwargs["device"]
            self._data["dev_uuid"] = kwargs["dev_uuid"]

        # self.fstype
        if "fstype" in kwargs:
            assert kwargs["fstype"] in [self.FS_TYPE_VFAT, self.FS_TYPE_EXT4, self.FS_TYPE_BTRFS, self.FS_TYPE_BCACHEFS, self.FS_TYPE_SQUASHFS_WITH_OVERLAY]
            self._data["fstype"] = kwargs["fstype"]
        else:
            assert self._data["device"] is not None
            if ":" in kwargs["device"]:
                for item in kwargs["device"].split(":"):
                    if "fstype" not in self._data:
                        self._data["fstype"] = Util.getBlkDevFsType(item)
                        assert self._data["fstype"] in [self.FS_TYPE_BTRFS, self.FS_TYPE_BCACHEFS]       # only these 2 file systems support multi-volume
                    else:
                        assert self._data["fstype"] == Util.getBlkDevFsType(item)
            else:
                self._data["fstype"] = Util.getBlkDevFsType(item)
                if self._data["fstype"] not in [self.FS_TYPE_VFAT, self.FS_TYPE_EXT4, self.FS_TYPE_BTRFS, self.FS_TYPE_BCACHEFS]:
                    # self.FS_TYPE_SQUASHFS_WITH_OVERLAY is not here, it is not for auto detection
                    raise RunningEnvironmentError("device \"%s\" has unsupported filesystem %s" % (item, self._data["fstype"]))

        # self.opts
        if "opts" in kwargs:
            assert kwargs["fstype"] is not None
            self._data["opts"] = kwargs["opts"]
        else:
            assert "fstype" not in kwargs
            assert self._data["device"] is not None
            self._data["opts"] = PhysicalDiskMounts.find_entry_by_mount_point(kwargs["mountpoint"]).opts
        if self._data["device"] is None:
            # "device=" can not be used for offline scenario, use information in self.underlay_disk
            if kwargs["fstype"] == self.FS_TYPE_BTRFS:
                assert self._data["opts"] == Util.mntOptsRemovePattern(self._data["opts"], "device=.*")
            elif kwargs["fstype"] == self.FS_TYPE_BCACHEFS:
                assert False

        # self.underlay_disk
        if "underlay_disk" in kwargs:
            assert "fstype" in kwargs and "opts" in kwargs
            self._data["underlay_disk"] = kwargs["underlay_disk"]
            HostDisk._assertData(self._data["underlay_disk"], [], False)
        else:
            assert self._data["device"] is not None
            self._data["underlay_disk"] = _getUnderlayDisk([HostDiskScsiHdd,                # this line is in the first, because:
                                                            HostDiskBtrfsRaid,              # this line may be stripped by ebuild file
                                                            HostDiskBcachefsRaid,           # this line may be stripped by ebuild file
                                                            HostDiskLvmLv,                  # this line may be stripped by ebuild file
                                                            HostDiskBcache,                 # this line may be stripped by ebuild file
                                                            HostDiskNvmeHdd,
                                                            HostDiskXenHdd,
                                                            HostDiskVirtioHdd], self._data["device"], mountPoint=self._data["mountpoint"])
            HostDisk._assertData(self._data["underlay_disk"], [], True)

    @property
    def mountpoint(self):
        # use same variable name as the namedtuple elements in psutil.disk_partitions()
        return self._data["mountpoint"]

    @property
    def device(self):
        # use same variable name as the namedtuple elements in psutil.disk_partitions()
        return self._data["device"]

    @property
    def fstype(self):
        # use same variable name as the namedtuple elements in psutil.disk_partitions()
        return self._data["fstype"]

    @property
    def opts(self):
        # use same variable name as the namedtuple elements in psutil.disk_partitions()
        return self._data["opts"]

    @property
    def dev_uuid(self):
        return self._data["dev_uuid"]

    @property
    def underlay_disk(self):
        return HostDisk._getClass(self._data["underlay_disk"])(self._data["underlay_disk"], [])

    def is_online(self):
        return self._data["device"] is not None

    def to_offline_mount_point(self):
        data = copy.deepcopy(self._data)

        if data["device"] is None:
            return HostMountPoint(**data)

        data["device"] = None
        if data["fstype"] == self.FS_TYPE_BTRFS:
            data["opts"] = Util.mntOptsRemovePattern(data["opts"], "device=.*")
        elif data["fstype"] == self.FS_TYPE_BCACHEFS:
            assert False

        for d, _ in HostDisk._walk(data["underlay_disk"], [], "pre-order"):
            HostDisk._getClass(d)._setOfflineImpl(d)

        return HostMountPoint(**data)

    def to_dict(self):
        return self._data

    def __getitem__(self, key):
        return self._data[key]

    def __iter__(self):
        return iter(self._data)

    def __len__(self):
        return len(self._data)


class HostDisk(abc.ABC):

    def __init__(self, data, parentDataPointerList):
        self._data = data
        self._parentDataPointerList = parentDataPointerList

    @property
    def uuid(self):
        return self._data["uuid"]

    @property
    def parent(self):
        if len(self._parentDataPointerList) > 0:
            return self._getClass(self._parentDataPointerList[-1])(self._parentDataPointerList[-1], self._parentDataPointerList[:-1])
        else:
            return None

    @property
    def children(self):
        return [self._getClass(child)(child, [self._data] + self._parentDataPointerList) for child in self._data["children"]]

    def walk(self, order):
        for data, parentList in self._walk(self._data, self._parentDataPointerList, order):
            yield self._getClass(data)(data, parentList)

    def __eq__(self, other):
        return type(self) is type(other) and self._data["uuid"] == other._data["uuid"]

    def __hash__(self):
        return hash(self._data["uuid"])

    @staticmethod
    def _getClass(data):
        if data["type"] == "btrfs-raid":
            return HostDiskBtrfsRaid
        elif data["type"] == "bcachefs-raid":
            return HostDiskBcachefsRaid
        elif data["type"] == "lvm-lv":
            return HostDiskLvmLv
        elif data["type"] == "bcache":
            return HostDiskBcache
        elif data["type"] == "scsi-hdd":
            return HostDiskScsiHdd
        elif data["type"] == "nvme-hdd":
            return HostDiskNvmeHdd
        elif data["type"] == "xen-hdd":
            return HostDiskXenHdd
        elif data["type"] == "virtio-hdd":
            return HostDiskVirtioHdd
        elif data["type"] == "image-file-on-removable":
            return HostDiskImageFileOnRemovable
        else:
            assert False

    @classmethod
    def _walk(cls, data, parentDataPointerList, order):
        if order == "pre-order":
            yield data, parentDataPointerList
            for child in data["children"]:
                yield from cls._walk(child, [data] + parentDataPointerList, order)
        elif order == "post-order":
            for child in data["children"]:
                yield from cls._walk(child, [data] + parentDataPointerList, order)
            yield data, parentDataPointerList
        else:
            assert False

    @staticmethod
    @abc.abstractmethod
    def _genData(devPath, mountPoint):
        pass

    @classmethod
    def _assertData(cls, data, parentDataPointerList, bOnlineOrOffline):
        cls._getClass(data)._assertDataImpl(data, bOnlineOrOffline)
        assert re.fullmatch(r'(UUID=|UUID_SUB=)\S+', data["uuid"])
        for childData in data["children"]:
            cls._assertData(childData, [data] + parentDataPointerList, bOnlineOrOffline)

    @staticmethod
    @abc.abstractmethod
    def _assertDataImpl(data, bOnlineOrOffline):
        pass

    @staticmethod
    @abc.abstractmethod
    def _setOfflineImpl(data):
        pass

    @staticmethod
    def _getSubUuidOrUuidWithPrefix(devPath):
        ret = Util.getBlkDevSubUuid(devPath)
        if ret is not None:
            return "UUID_SUB=" + ret
        ret = Util.getBlkDevUuid(devPath)
        if ret is not None:
            return "UUID=" + ret
        assert False


class HostDiskFeatureUsePartitionTable:

    PARTITION_TABLE_TYPE_MBR = "mbr"
    PARTITION_TABLE_TYPE_GPT = "gpt"

    @property
    def used_partition_table_types(self):
        return self._data["used_partition_table_types"]

    @classmethod
    def _getPartitionType(cls, diskDevPath, partitionId):
        if partitionId is None:
            # FIXME: currently this branch leads to error
            return []
        else:
            m = re.search(r'(^| )PTTYPE="(\S+)"', subprocess.check_output(["blkid", diskDevPath], text=True), re.M)
            if m.group(2) == "dos":
                return cls.PARTITION_TABLE_TYPE_MBR
            elif m.group(2) == "gpt":
                return cls.PARTITION_TABLE_TYPE_GPT
            else:
                raise RunningEnvironmentError("unknown partition type \"%s\" for block device \"%s\"" % (m.group(2), diskDevPath))

    @classmethod
    def _assertDataFeature(cls, data):
        assert all(x in [cls.PARTITION_TABLE_TYPE_MBR, cls.PARTITION_TABLE_TYPE_GPT] for x in data["used_partition_table_types"])


class HostDiskBtrfsRaid(HostDisk):

    @property
    def slave_dev_list(self):
        return self.children

    @staticmethod
    def _genData(devPath, mountPoint):
        if mountPoint is None:
            return None
        if not os.path.exists(devPath):
            return None
        if Util.getBlkDevFsType(devPath) != "btrfs":
            return None

        slaveDevPathList = Util.btrfsGetSlavePathList(mountPoint)
        assert len(slaveDevPathList) > 0

        # uuid: FS-UUID of the whole btrfs filesystem
        ret = {
            "type": "btrfs-raid",
            "uuid": "UUID=%s" % (Util.btrfsGetUuid(mountPoint)),
            "children": [],
        }
        for slaveDevPath in slaveDevPathList:
            ret["children"].append(_getUnderlayDisk([HostDiskBcache,
                                                     HostDiskScsiHdd,
                                                     HostDiskNvmeHdd,
                                                     HostDiskXenHdd,
                                                     HostDiskVirtioHdd], slaveDevPath))
        return ret

    @staticmethod
    def _assertDataImpl(data, bOnlineOrOffline):
        assert len(data["children"]) > 0

    @staticmethod
    def _setOfflineImpl(data):
        pass


class HostDiskBcachefsRaid(HostDisk):

    @property
    def slave_dev_list(self):
        return self.children

    @staticmethod
    def _genData(devPath, mountPoint):
        if mountPoint is None:
            return None

        slaveDevPathList = devPath.split(":")
        if any(Util.getBlkDevFsType(x) != "bcachefs" for x in slaveDevPathList):
            return None
        assert len(slaveDevPathList) > 0
        assert all(Util.getBlkDevFsType(x) == "bcachefs" for x in slaveDevPathList)

        # uuid: FS-UUID of the whole bcachefs filesystem
        ret = {
            "type": "bcachefs-raid",
            "uuid": "UUID=%s" % (Util.bcachefsGetUuid(slaveDevPathList)),
            "children": [],
        }
        for slaveDevPath in slaveDevPathList:
            ret["children"].append(_getUnderlayDisk([HostDiskBcache,
                                                     HostDiskScsiHdd,
                                                     HostDiskNvmeHdd,
                                                     HostDiskXenHdd,
                                                     HostDiskVirtioHdd], slaveDevPath))
        return ret

    @staticmethod
    def _assertDataImpl(data, bOnlineOrOffline):
        assert len(data["children"]) > 0

    @staticmethod
    def _setOfflineImpl(data):
        pass


class HostDiskLvmLv(HostDisk):

    @property
    def vg_name(self):
        return self._data["vg_name"]

    @property
    def lv_name(self):
        return self._data["lv_name"]

    @property
    def slave_dev_list(self):
        return self.children

    @staticmethod
    def _genData(devPath, mountPoint):
        lvmInfo = Util.getBlkDevLvmInfo(devPath)
        if lvmInfo is None:
            return None
        assert mountPoint is not None

        slaveDevPathList = Util.lvmGetSlaveDevPathList(lvmInfo[0])
        assert len(slaveDevPathList) > 0

        # uuid: FS-UUID of the filesystem in this LV, or SUB-UUID takes priority for multi-volume filesystem
        ret = {
            "type": "lvm-lv",
            "uuid": Util.lvmGetUuid(mountPoint),
            "vg_name": lvmInfo[0],
            "lv_name": lvmInfo[1],
            "children": [],
        }
        for slaveDevPath in slaveDevPathList:
            ret["children"].append(_getUnderlayDisk([HostDiskBcache,
                                                     HostDiskScsiHdd,
                                                     HostDiskNvmeHdd,
                                                     HostDiskXenHdd,
                                                     HostDiskVirtioHdd], slaveDevPath))
        return ret

    @staticmethod
    def _assertDataImpl(data, bOnlineOrOffline):
        assert "vg_name" in data
        assert "lv_name" in data
        assert len(data["children"]) > 0

    @staticmethod
    def _setOfflineImpl(data):
        pass


class HostDiskBcache(HostDisk):

    @property
    def cache_set_uuid(self):
        return self._data["cache_set_uuid"]

    @property
    def cache_dev_list(self):
        return self.children[:-1]

    @property
    def backing_dev(self):
        return self.children[-1]

    @property
    def dev(self):
        return self._data["dev"]

    @classmethod
    def _genData(cls, devPath, mountPoint):
        m = re.fullmatch("/dev/bcache[0-9]+", devPath)
        if m is None:
            return None

        slaveDevPathList = Util.bcacheGetSlaveDevPathList(devPath)
        assert len(slaveDevPathList) > 0

        # uuid: FS-UUID of the filesystem in this bcache device, or SUB-UUID takes priority for multi-volume filesystem
        ret = {
            "type": "bcache",
            "uuid": cls._getSubUuidOrUuidWithPrefix(devPath),
            "cache_set_uuid": None,
            "dev": devPath,
            "children": [],
        }
        for i, slaveDevPath in enumerate(slaveDevPathList):
            if i < len(slaveDevPathList) - 1:
                if ret["cache_set_uuid"] is not None:
                    assert ret["cache_set_uuid"] == Util.bcacheGetSetUuid(slaveDevPath)
                else:
                    ret["cache_set_uuid"] = Util.bcacheGetSetUuid(slaveDevPath)
            ret["children"].append(_getUnderlayDisk([HostDiskScsiHdd,
                                                     HostDiskNvmeHdd,
                                                     HostDiskXenHdd,
                                                     HostDiskVirtioHdd], slaveDevPath))
        return ret

    @staticmethod
    def _assertDataImpl(data, bOnlineOrOffline):
        assert "cache_set_uuid" in data
        assert len(data["children"]) > 0
        assert ("dev" in data) == bOnlineOrOffline

    @staticmethod
    def _setOfflineImpl(data):
        if "dev" in data:
            del data["dev"]


class HostDiskScsiHdd(HostDisk, HostDiskFeatureUsePartitionTable):

    @property
    def host_controller_name(self):
        return self._data["host_controller_name"]

    @property
    def should_disable_write_cache(self):
        return self._data["need_disable_cache"]

    @should_disable_write_cache.setter
    def should_disable_write_cache(self, value):
        self._data["need_disable_cache"] = bool(value)

    @property
    def dev(self):
        return self._data["dev"]

    @classmethod
    def _genData(cls, devPath, mountPoint):
        m = re.fullmatch("(/dev/sd[a-z])([0-9]+)?", devPath)
        if m is not None:
            # uuid: FS-UUID of the filesystem in this device, or SUB-UUID takes priority for multi-volume filesystem
            return {
                "type": "scsi-hdd",
                "uuid": cls._getSubUuidOrUuidWithPrefix(devPath),
                "used_partition_table_types": [cls._getPartitionType(m.group(1), m.group(2))],
                "host_controller_name": Util.scsiGetHostControllerName(m.group(1)),
                "need_disable_cache": False,
                "dev": devPath,
                "children": [],
            }
        else:
            return None

    @staticmethod
    def _assertDataImpl(data, bOnlineOrOffline):
        HostDiskFeatureUsePartitionTable._assertDataFeature(data)
        assert "host_controller_name" in data
        assert "need_disable_cache" in data
        assert ("dev" in data) == bOnlineOrOffline

    @staticmethod
    def _setOfflineImpl(data):
        if "dev" in data:
            del data["dev"]


class HostDiskNvmeHdd(HostDisk, HostDiskFeatureUsePartitionTable):

    @property
    def should_disable_write_cache(self):
        return self._data["need_disable_cache"]

    @should_disable_write_cache.setter
    def should_disable_write_cache(self, value):
        self._data["need_disable_cache"] = bool(value)

    @property
    def dev(self):
        return self._data["dev"]

    @classmethod
    def _genData(cls, devPath, mountPoint):
        m = re.fullmatch("(/dev/nvme[0-9]+n[0-9]+)(p([0-9]+))?", devPath)
        if m is not None:
            # uuid: FS-UUID of the filesystem in this device, or SUB-UUID takes priority for multi-volume filesystem
            return {
                "type": "nvme-hdd",
                "uuid": cls._getSubUuidOrUuidWithPrefix(devPath),
                "used_partition_table_types": [cls._getPartitionType(m.group(1), m.group(3))],
                "need_disable_cache": False,
                "dev": devPath,
                "children": [],
            }
        else:
            return None

    @staticmethod
    def _assertDataImpl(data, bOnlineOrOffline):
        HostDiskFeatureUsePartitionTable._assertDataFeature(data)
        assert "need_disable_cache" in data
        assert ("dev" in data) == bOnlineOrOffline

    @staticmethod
    def _setOfflineImpl(data):
        if "dev" in data:
            del data["dev"]


class HostDiskXenHdd(HostDisk, HostDiskFeatureUsePartitionTable):

    @property
    def dev(self):
        return self._data["dev"]

    @classmethod
    def _genData(cls, devPath, mountPoint):
        m = re.fullmatch("(/dev/xvd[a-z])([0-9]+)?", devPath)
        if m is not None:
            # uuid: FS-UUID of the filesystem in this device, or SUB-UUID takes priority for multi-volume filesystem
            return {
                "type": "xen-hdd",
                "uuid": cls._getSubUuidOrUuidWithPrefix(devPath),
                "used_partition_table_types": [cls._getPartitionType(m.group(1), m.group(2))],
                "dev": devPath,
                "children": [],
            }
        else:
            return None

    @staticmethod
    def _assertDataImpl(data, bOnlineOrOffline):
        HostDiskFeatureUsePartitionTable._assertDataFeature(data)
        assert ("dev" in data) == bOnlineOrOffline

    @staticmethod
    def _setOfflineImpl(data):
        if "dev" in data:
            del data["dev"]


class HostDiskVirtioHdd(HostDisk, HostDiskFeatureUsePartitionTable):

    @property
    def dev(self):
        return self._data["dev"]

    @classmethod
    def _genData(cls, devPath, mountPoint):
        m = re.fullmatch("(/dev/vd[a-z])([0-9]+)?", devPath)
        if m is not None:
            # uuid: FS-UUID of the filesystem in this device, or SUB-UUID takes priority for multi-volume filesystem
            return {
                "type": "virtio-hdd",
                "uuid": cls._getSubUuidOrUuidWithPrefix(devPath),
                "used_partition_table_types": [cls._getPartitionType(m.group(1), m.group(2))],
                "dev": devPath,
                "children": [],
            }
        else:
            return None

    @staticmethod
    def _assertDataImpl(data, bOnlineOrOffline):
        HostDiskFeatureUsePartitionTable._assertDataFeature(data)
        assert ("dev" in data) == bOnlineOrOffline

    @staticmethod
    def _setOfflineImpl(data):
        if "dev" in data:
            del data["dev"]


class HostDiskImageFileOnRemovable(HostDisk, HostDiskFeatureUsePartitionTable):

    @property
    def image_file_path(self):
        return self._data["image_file_path"]

    @property
    def dev(self):
        return self._data["dev"]

    @classmethod
    def _genData(cls, uuid, imageFilePath, devPath):
        # uuid: FS-UUID of the filesystem in this device
        return {
            "type": "image-file-on-removable",
            "uuid": "UUID=%s" % (uuid),
            "used_partition_table_types": [cls.PARTITION_TABLE_TYPE_MBR, cls.PARTITION_TABLE_TYPE_GPT],
            "image_file_path": imageFilePath,
            "dev": devPath,
            "children": [],
        }

    @staticmethod
    def _assertDataImpl(data, bOnlineOrOffline):
        HostDiskFeatureUsePartitionTable._assertDataFeature(data)
        assert "image_file_path" in data and not data["image_file_path"].startswith("/")
        assert ("dev" in data) == bOnlineOrOffline

    @staticmethod
    def _setOfflineImpl(data):
        if "dev" in data:
            del data["dev"]


class AuxOs:

    class Type(enum.Enum):
        MICROSOFT_WINDOWS = "microsoft-windows"

    def __init__(self, name, os_type, partition_path_or_uuid, chainloader_number):
        assert isinstance(os_type, self.Type)

        self.name = name
        self.os_type = os_type
        if partition_path_or_uuid.startswith("/dev/"):
            self.partition_path = partition_path_or_uuid
            self.partition_uuid = "UUID=" + Util.getBlkDevUuid(self.partition_path)
        else:
            self.partition_path = None
            self.partition_uuid = partition_path_or_uuid
        self.chainloader_number = chainloader_number

    def get_paths_in_boot_dir(self):
        if self.os_type == self.OS_TYPE_MICROSOFT_WINDOWS:
            return [
                "EFI/Microsoft",
                "McAfee",
            ]
        else:
            assert False

    def __eq__(self, other):
        if type(self) is not type(other):
            return False
        if self.name != other.name:
            return False
        if self.os_type != other.os_type:
            return False
        if self.partition_uuid != other.partition_uuid:
            return False
        if self.chainloader_number != other.chainloader_number:
            return False
        if self.partition_path is not None and other.partition_path is not None:
            assert self.partition_path == other.partition_path
        return True

    def __hash__(self):
        return hash((self.name, self.os_type, self.partition_uuid, self.chainloader_number))


class FsLayout:

    def get_boot_dir(self):
        return "/boot"

    def get_boot_efi_dir(self):
        return os.path.join(self.get_boot_dir(), "EFI")

    def get_boot_grub_dir(self):
        return os.path.join(self.get_boot_dir(), "grub")

    def get_boot_rescue_os_dir(self):
        return os.path.join(self.get_boot_dir(), "rescue")

    def get_boot_history_dir(self):
        return os.path.join(self.get_boot_dir(), "history")

    def get_kernel_modules_dir(self, kernel_verstr=None):
        if kernel_verstr is None:
            return "/lib/modules"
        else:
            return "/lib/modules/%s" % (kernel_verstr)

    def get_firmware_dir(self):
        return "/lib/firmware"

    def get_bbki_cache_dir(self):
        return "/var/cache/bbki"

    def get_bbki_cache_distfiles_dir(self):
        return os.path.join(self.get_bbki_cache_dir(), "distfiles")

    def get_bbki_cache_distfiles_ro_dir_list(self):
        return []

    def get_bbki_tmp_dir(self):
        return "/var/tmp/bbki"


@staticmethod
def _getUnderlayDisk(underlayDiskTypeList, devPath, mountPoint=None):
    for klass in underlayDiskTypeList:
        ret = klass._genData(devPath, mountPoint)
        if ret is not None:
            return ret

    # unknown
    raise RunningEnvironmentError("unknown device \"%s\"" % (devPath))
